Error Handling
web3agent has two error surfaces: the MCP tool layer (used by AI agents) and the SDK layer (used by TypeScript/JavaScript code). Both follow consistent patterns, but the shape of the error differs.
MCP Tool Errors
When a tool call fails, the MCP server returns a structured error response instead of throwing:
// Success
{ ok: true, data: { ... } }
// Failure
{
ok: false,
error: {
code: "SWAP_QUOTE_FAILED",
message: "Insufficient liquidity for this pair on Base"
}
}
AI agents receive this response and can decide how to handle it (retry, ask the user, try a different approach). The code field is a stable string identifier; message is human-readable.
SDK Errors
SDK functions throw Web3AgentError when something goes wrong. This is a subclass of Error with two additional fields:
class Web3AgentError extends Error {
code: string; // stable error code
message: string; // human-readable description
}
Import and use it like this:
import { getSwapQuote, Web3AgentError } from "web3agent";
try {
const quote = await getSwapQuote({
fromToken: "0x833589...",
toToken: "0x4200000...",
fromAmount: "100000000",
chainId: 8453,
});
console.log(quote);
} catch (e: unknown) {
if (e instanceof Web3AgentError) {
// Structured error from web3agent
console.error(`[${e.code}] ${e.message}`);
} else {
// Unexpected error (network, runtime, etc.)
throw e;
}
}
Common Error Codes
| Code | Cause | Resolution |
|---|---|---|
CHAIN_NOT_SUPPORTED | Chain ID not in the supported list | Check supported chains |
WALLET_NOT_CONFIGURED | Write operation attempted with no wallet | Set PRIVATE_KEY or MNEMONIC |
WALLET_NOT_ACTIVE | No wallet is currently active | Call wallet_activate first |
POLICY_REJECTED | Transaction exceeds spend limits | Increase POLICY_MAX_* or reduce trade size |
CONFIRMATION_REQUIRED | Write queued, awaiting confirmation | Call transaction_confirm with the returned ID |
CONFIRMATION_EXPIRED | Confirmation TTL elapsed | Re-initiate the operation |
SWAP_QUOTE_FAILED | Orbs Liquidity Hub returned no quote | Try a different pair, amount, or chain |
INSUFFICIENT_LIQUIDITY | Not enough liquidity for the swap | Reduce amount or try a different route |
LOW_DOLLAR_VALUE | Trade amount below minimum (~$6-10) | Increase the trade size |
TOKEN_NOT_FOUND | Symbol or address not in registry | Use the full token address instead of symbol |
BLOCKSCOUT_NOT_SUPPORTED | Explorer tool called on unsupported chain | Use an EVM tool instead, or switch to a supported chain |
RPC_ERROR | Node RPC call failed | Check RPC_URL, try a different endpoint |
INVALID_INPUT | Zod schema validation failed | Check the parameter types and required fields |
BRIDGE_QUOTE_FAILED | LI.FI returned no route | Try different tokens, chains, or amounts |
OPERATION_NOT_FOUND | operation_resume called with unknown ID | Re-prepare the operation |
TOOL_NOT_FOUND | Tool name doesn't exist | Check the tool catalog |
Tool Error Utilities
When building custom tools or handlers, use these utilities from src/utils/:
formatToolError(code, message)
Creates a standardized { ok: false, error: { code, message } } response.
import { formatToolError } from "web3agent";
return formatToolError("MY_ERROR_CODE", "Something went wrong");
formatToolErrorFromUnknown(code, error)
Converts an unknown caught value into a tool error response. Handles Error instances, strings, and arbitrary objects.
import { formatToolErrorFromUnknown } from "web3agent";
try {
// ... some operation
} catch (e: unknown) {
return formatToolErrorFromUnknown("OPERATION_FAILED", e);
}
Best Practices
Always type the catch variable as unknown
TypeScript's catch (e: unknown) is the correct pattern. Never use a bare catch (e) or catch (e: any).
// Correct
try {
await someOperation();
} catch (e: unknown) {
if (e instanceof Web3AgentError) {
// handle structured error
} else if (e instanceof Error) {
// handle generic error
} else {
// handle unknown
}
}
// Wrong — don't do this
try {
await someOperation();
} catch (e) {
console.error(e.message); // TypeScript error: 'e' is of type 'unknown'
}
Don't swallow errors silently
If you catch an error and can't handle it meaningfully, re-throw it. Silent catches hide bugs.
// Wrong
try {
await riskyOperation();
} catch {
// nothing
}
// Better
try {
await riskyOperation();
} catch (e: unknown) {
console.error("riskyOperation failed:", e);
throw e;
}
Check ok before accessing data
When calling tools directly via runtime.invokeTool(), always check the ok field before accessing data:
const result = await runtime.invokeTool("resolve_token", { symbol: "USDC", chainId: 8453 });
if (!result.ok) {
throw new Error(`resolve_token failed: [${result.error.code}] ${result.error.message}`);
}
const token = result.data;
The SDK functions (getSwapQuote, prepareSwapIntent, etc.) do this check internally and throw Web3AgentError for you, so you only need to handle the thrown error.
Error Handling in CLI Context
When running as an MCP server via npx web3agent, errors are written to stderr with a [module-name] prefix. The MCP client (Claude Code, Cursor, etc.) receives the structured error response and decides how to surface it to the user.
[orbs] swap quote failed: insufficient liquidity for USDC→WETH on Base
[policy] transaction rejected: $450 exceeds single-tx limit of $100
[wallet] no active wallet — set PRIVATE_KEY or call wallet_activate
stdout is reserved exclusively for the MCP protocol. Never write to stdout in custom integrations.